package util;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import model.NotificationManager;

/**
 * Listens to the file system change notifications and raises events when a directory,
 * or file in a directory, changes. Note that this only works on Windows 2000, XP, or later
 * operating system, and you need the dynamic link library FileSystemWatch.dll file.
 *
 * @author (Cedric) Qin ZHANG, Modified by Eric
 * @version $Id$
 */
public class FileSystemWatcher {

    /** Directory to watch for. */
    private String dirPath;
    /** The worker thread monitoring file system changes. */
    private Worker worker = null;
    private int frequency;
    private int targetDirHashcode;
    private String expectedFilesPattern;
    private List allChangedFiles;
    private HashMap allMatchedFiles = null;
    private boolean ifStopAfterFindExpected;
    private String detected;

    public String getDetected() {
        if (this.allChangedFiles!=null && this.allChangedFiles.size() > 0) {
            String file = this.allChangedFiles.get(0).toString();
            System.out.println(file.substring(file.lastIndexOf("\\")+1,file.lastIndexOf(".")));
            return file.substring(file.lastIndexOf("\\")+1,file.lastIndexOf("."));
        } else {
            return null;
        }
    }

    /**
     * Initializes a new instance of this class.
     *
     * @param dirPath The path of the directory to watch. Note that the directory must exist.
     * @param notifyFilter The type of chagnes to watch for.
     * @param includeSubdirectories Whether subdirectories should be monitored.
     *
     * @throws IOException If dirPath does not exist and is not a directory.
     */
    public FileSystemWatcher(String dirPath, String expected, int frequency, boolean stopAfterFindExpected)
            throws IOException {
        if (!new File(dirPath).isDirectory()) {
            throw new IOException(dirPath + " is not a directory.");
        }
        this.expectedFilesPattern = expected;
        this.dirPath = dirPath;
        this.frequency = frequency;
        this.ifStopAfterFindExpected = stopAfterFindExpected;
//        this.ifStopAfterFindExpected = stopAfterFindExpected;
    }

    public HashMap getAllMatchedFiles() {
//        System.out.println("getAllMatechedFiles.................");
        HashMap result = new HashMap();
        List allFiles = getAllFiles(this.dirPath);
        Iterator it = allFiles.iterator();
        while (it.hasNext()) {
            WatchedFile file = (WatchedFile) it.next();
            Pattern p = Pattern.compile(this.expectedFilesPattern);
            Matcher matcher = p.matcher(file.getName());
            if (matcher.matches()) {
                result.put(file, file.hashCode());
            }
//            Pattern p = Pattern.compile(this.expectedFilesPattern);
//            Matcher matcher = p.matcher(file.getName());
//            if (matcher.matches()) {
////                System.out.println("->"+file.getAbsolutePath());
//                result.put(file, file.hashCode());
//            }
        }
//        System.out.println(result.size());
        return result;
    }

    private List getAllFiles(String path) {
        List allFiles = new ArrayList();
        File f = new File(path);
        File[] files = f.listFiles();
        for (int i = 0, n = files.length; i < n; i++) {
            if (files[i].isDirectory()) {
                List filesInSubDir = getAllFiles(files[i].getAbsolutePath());
                Iterator it = filesInSubDir.iterator();
                while (it.hasNext()) {
                    allFiles.add((WatchedFile) it.next());
                }
            }
//            System.out.println(files[i].getAbsolutePath());
            allFiles.add(new WatchedFile(files[i].getAbsolutePath()));
        }
        return allFiles;
    }

    /**
     * Starts watching changes in the directory. Note that a worker thread will be started to
     * monitor file system changes, and this method will return immediately. All file system events
     * are fired in the worker thread. The worker thread will run until stop() is invoked if there
     * is no error. In case of error, the worker thread might terminate prematurely. You can use
     * isRunning() to check whether the worker thread is running or not.
     */
    public synchronized void start() {
        if (this.worker != null) {
            throw new IllegalStateException("The FileSystemWatcher object is already in running state.");
        }
        this.worker = new Worker(this);
//        this.worker.setDaemon(true);
        this.targetDirHashcode = getTargetHashcode();
        this.allMatchedFiles = getAllMatchedFiles();
        this.worker.start();
        System.out.println(this.getDetected());
    }

    public void setHashcode(int hashcode) {
        this.targetDirHashcode = hashcode;
    }

    /**
     * Stops watching changes in the directory.
     */
    public synchronized void stop() throws IllegalStateException {
        if (this.worker != null) {
            this.worker.requestTermination();
            this.worker = null;
        }
    }

    public synchronized boolean isRunning() {
        return this.worker != null && this.worker.isAlive();
    }

    private int getTargetHashcode() {
        WatchedFile target = new WatchedFile(this.dirPath);
        return target.hashCode();
    }

    private void notifyChanges() {
//        System.out.println("Changes detected:");
        for (int i = 0; i < this.allChangedFiles.size(); i++) {
            String file = (String) allChangedFiles.get(i);
//            System.out.println(file);
            NotificationManager.addNotification("watch " + this.expectedFilesPattern, file);
        }
    }

    private void setFound(List compareHash) {
        this.allChangedFiles = compareHash;
    }

    /**
     * Worker thread monitoring file system changes.
     *
     * @author (Cedric) Qin ZHANG
     * @version $Id$
     */
    private class Worker extends Thread {

        private boolean terminationRequested = false;
        private FileSystemWatcher parent;

        public Worker(FileSystemWatcher fsw) {
            this.parent = fsw;
        }

        private List compareHash(HashMap candidatesHash, HashMap map) {
            List result = new ArrayList();
//            System.out.println(candidatesHash.size()+":"+map.size());
            Iterator it = map.keySet().iterator();
            while (it.hasNext()) {
                WatchedFile key = (WatchedFile) it.next();
                if (!candidatesHash.containsKey(key)) {
//                    System.out.println("new file: " + key.getAbsolutePath());
                    result.add(key.getAbsolutePath());
                } else {
//                    if (candidatesHash.get(key).toString().equals(map.get(key).toString()) ) {
//                        System.out.println("changed file: "+key.getAbsolutePath()+" "+candidatesHash.get(key)+" to "+map.get(key));
//                        result.add(key.getAbsolutePath() + "\\" + key.getName());
//                    }
                }
            }
//            System.out.println(result.size());
            return result;
        }

        /**
         * Requests the worker thread to stop monitoring file system changes.
         */
        private synchronized void requestTermination() {
            this.terminationRequested = true;
        }

        /**
         * Checks whether any request has been received asking the working thread stop monitoring file
         * system changes.
         *
         * @return True if at least one request has been received.
         */
        private synchronized boolean isTerminationRequested() {
            return this.terminationRequested;
        }

        /**
         * Thread entry point. It monitors file system changes until a termination request is
         * received, or an exception is encountered.
         */
        @Override
        public void run() {
            while (true) {
                try {
                    sleep(this.parent.frequency * 1000);
                } catch (InterruptedException ex) {
                    Logger.getLogger(FileSystemWatcher.class.getName()).log(Level.SEVERE, null, ex);
                }
                if (terminationRequested) {
                    break;
                }
                if (this.parent.targetDirHashcode != this.parent.getTargetHashcode()) {
                    HashMap map = this.parent.getAllMatchedFiles();
                    List changes = compareHash(this.parent.allMatchedFiles, map);
                    if (changes.size() > 0) {
                        this.parent.setFound(changes);
                        this.parent.notifyChanges();
                        this.parent.detected = this.parent.getDetected();
                        if (this.parent.ifStopAfterFindExpected) {
                            break;
                        }
                    }
                    this.parent.targetDirHashcode = this.parent.getTargetHashcode();
                    this.parent.allMatchedFiles.clear();
                    this.parent.allMatchedFiles = null;
                    this.parent.allMatchedFiles = this.parent.getAllMatchedFiles();
                }
            }
        }

        /**
         * Method called by Java garbage collector to dispose operating system resource.
         */
        @Override
        protected void finalize() throws Throwable {
        }
    }

    public static void main(String[] args) {
        try {
            FileSystemWatcher watcher = new FileSystemWatcher("c:\\a", "(?i)t[1-9]st.*", 5, true);
            watcher.start();
//            System.out.println(watcher.getDetected());
        } catch (IOException ex) {
            Logger.getLogger(FileSystemWatcher.class.getName()).log(Level.SEVERE, null, ex);
        }

    }
}
